我們已經介紹了toSignal
和toObservable
,並了解了signals
和Observables
如何互通以實現所需的結果。 今天,我將介紹ngXtension
庫中的toObservableSignal
實用函數,它結合了signals
和 Observables
的功能。
然後,我重建了第6天的範例,應用toObservableSignal
發出HTTP請求來檢索《星際大戰》字元並分別建立一個番茄計時器。
npm install ngxtension
import { toObservableSignal } from 'ngxtension/to-observable-signal';
例子:
id = toObservableSignal(signal(10));
nextId = computed(() => this.id() + 1);
double = computed(() => this.id() * 2);
addOne$ = this.id.pipe(map((id) => id + 1));
<p>{{ id() }}, {{ nextId() }}, {{ double() }}</p> // 10, 11, 20
<p>{{ addOne$ | async }}<p> // 11
要使用toObservableSignal
,我們必須從ngxtension/to-observable-signal
導入。 toObservableSignal
接受一個signal,它表現出signal
和Observable
的行為。nextId
是一個唯讀signal
,它將id
增加1,而double
則將id
乘以 2。 addone$
Observable也將id
增加1。
在下面的範例中,我根據HTML輸入欄位中的id
檢索Star War角色。 id
ObservableSignal會向各種 RxJS運算子發出以檢索字元。 然後,我呼叫forkJoin
來檢索該角色出現的影片。
export type Person = {
name: string;
height: string;
mass: string;
hair_color: string;
skin_color: string;
eye_color: string;
gender: string;
films: string[];
}
建立一個 Person 類型來保存 Star War 角色的實例。
const URL = 'https://swapi.dev/api/people';
export function getPerson(id: number, injector: Injector) {
return runInInjectionContext(injector, () => {
const http = inject(HttpClient);
return http.get<Person>(`${URL}/${id}`).pipe(
catchError((err) => {
console.error(err);
return of(undefined);
}));
});
}
定義getPerson
函數以根據id
檢索Star War角色。
export function getFilmTitle(url: string, injector: Injector): Observable<string> {
return runInInjectionContext(injector, () => {
const http = inject(HttpClient);
return http.get<{ title: string }>(url)
.pipe(
map(({ title }) => title),
catchError((err) => {
console.error(err);
return of('');
})
);
});
}
定義一個getFilmTitle
函數,該函數接受電影URL
並呼叫Star War API來檢索電影標題。
然後,我將在組件中匯入這兩個函數來檢索要顯示的角色和電影標題。
export class App {
id = toObservableSignal(signal(10));
nextId = computed(() => this.id() + 1);
injector = inject(Injector);
person$ = this.id.pipe(
debounceTime(300),
distinctUntilChanged(),
filter((v) => v >= 1 && v <= 83),
switchMap((v) => getPerson(v, this.injector)),
shareReplay(1),
);
films$ = this.person$.pipe(
map((p) => {
const films = p ? p.films : [];
return films.map((url) => getFilmTitle(url, this.injector));
}),
concatMap((x) => forkJoin(x)),
);
}
id
ObservableSignal將值傳送到
id
與當前id
不同時繼續id
是否在1到83之間film$
Observable 從角色中提取電影URL,將getFilmTitle
Observables傳遞給forkJoin
來取得數值,並使用concatMap
展平內部Observables。
<div>
<label for="id">
<span>Id: </span>
<input id="id" name="id" type="number" min="1" max="83" [(ngModel)]="id">
</label>
</div>
<div>
@if(person$ | async; as person) {
<p>Name: {{ person.name }}</p>
<p>Height: {{ person.height }}</p>
<p>Mass: {{ person.mass }}</p>
<p>Hair Color: {{ person.hair_color }}</p>
<p>Skin Color: {{ person.skin_color }}</p>
<p>Eye Color: {{ person.eye_color }}</p>
<p>Gender: {{ person.gender }}</p>
} @else {
<p>No info</p>
}
@if (films$ | async; as films) {
<p>Movies</p>
@for(film of films; track film) {
<ul style="padding-left: 1rem;">
<li>{{ film }}</li>
</ul>
}
} @else {
<p>No movie</p>
}
</div>
此範本使用async pipe
解析Observables並顯示結果。
nextId = computed(() => this.id() + 1)
nextId
是一個computed signal,等於目前id
加1。 然後,在範本中顯示nextId
的值。
Next Id: {{ nextId() }}
ObservableSignal
也是一個signl
;因此,它可以呼叫set()
或update()
來覆寫或計算signal
值。
<div style="margin-bottom: 1rem;">
<button (click)="id.set(1)">Luke Skywalker</button>
<button (click)="id.set(4)">Darth Vader</button>
<button (click)="id.set(5)">Princess Leia</button>
<button (click)="id.set(14)">Han Solo</button>
<button (click)="id.set(10)">Obi-Wan</button>
<button (click)="id.set(20)">Yoda</button>
</div>
這些按鈕將ObservableSignal
設定為不同的id
,以檢索流行的星際大戰角色,例如Luke Skywalker和Darth Vader。
function buildTimerString(currentSeconds: number) {
const secondsInHour = 3600;
const secondsInMinute = 60;
const hours = Math.floor(currentSeconds / secondsInHour);
const minutes = Math.floor((currentSeconds - hours * secondsInHour) / secondsInMinute);
const seconds = currentSeconds - hours * secondsInHour - minutes * secondsInMinute;
const padHours = hours < 10 ? `0${hours}` : `${hours}`;
const padMinutes = minutes < 10 ? `0${minutes}` : `${minutes}`;
const padSeconds = seconds < 10 ? `0${seconds}` : `${seconds}`;
return `${padHours}:${padMinutes}:${padSeconds}`;
}
buildTimerString
是一個實用程式函數,它接受整數並建立<hours>:<months>:<seconds>
格式的字串。 例如,10秒錶示為"00:00:10",4000 秒錶示為"01:06:40"。
export class App {
amount = signal(60);
unit = signal('1');
totalSeconds = toObservableSignal
(computed(() => this.amount() * parseInt(this.unit(), 10)));
timer$ = this.totalSeconds.pipe(
debounceTime(300),
switchMap((x) => timer(0, 1000).pipe(
map((y) => x - y),
take(x + 1),
)),
shareReplay(1),
);
timerString$ = this.timer$.pipe(map((x) => buildTimerString(x)));
}
類似地,totalSeconds
ObservableSignal將值傳送到
timerString$
將總秒數對應到計時器字串,並將其顯示在範本中。
<div>
<div>
<label for="id">
<span>Id: </span>
<input id="id" name="id" type="number" min="1"
[(ngModel)]="amount">
</label>
<label for="unit">
<span>Unit: </span>
<select [(ngModel)]="unit">
<option value="1">seconds</option>
<option value="60">minutes</option>
<option value="3600">hours</option>
</select>
</label>
</div>
</div>
<p>{{ timerString$ | async }}</p>
amount
和unit
訊號double binded到ngModel
,以便對HTML控制項的任何變更都會寫回它們。
當任何signal
更新時,Angular都會重新計算等於總秒數的totalSeconds
signal。timer$
和timerString$
Observables追蹤totalSeconds
signal,並在範本中顯示新的計時器字串。
鐵人賽的第七天就這樣結束了。
Stackblitz Demo: